<?php

/*
 * Copyright 2008 the original author or authors.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#namespace tehframework\annotations\parsers\state;

#use tehframework\annotations\DocBlockDefinition;
#use tehframework\annotations\TagDefinition;
#use tehframework\annotations\AnnotationDefinition;
#use tehframework\annotations\parsers\ParserException;
#use tehframework\annotations\parsers\DocBlockParser;

class StateParser extends DocBlockParser
{
	/**
	 * @var array<ParserState::*,StateProcessor>
	 */
	private $stateToProcessorMap;
	
	/**
	 * @var ParserState::*
	 */
	private $state;
	
	/**
	 * @var DocBlockDefinition
	 */
	private $docBlockDef;
	
	/**
	 * @var char?
	 */
	public $prevToken;
	
	/**
	 * @var char
	 */
	public $currentToken;
	
	/**
	 * @var char?
	 */
	public $nextToken;
	
	/**
	 * @param array<ParserState::*,StateProcessor> $stateToProcessorMap
	 */
	public function __construct(array $stateToProcessorMap = array())
	{
		$this->stateToProcessorMap =
			$stateToProcessorMap + ParserState::createStateToProcessorMap();
	}
	
	/**
	 * @param  string $docBlock
	 * @return DocBlockDefinition
	 * @throws ParserException
	 */
	public function parseDocBlock($docBlock)
	{
		$this->prepareDocBlockString($docBlock);
		
		foreach ($this->stateToProcessorMap as $stateProcessor)
		{
			if ($stateProcessor instanceof Resettable)
			{
				$stateProcessor->reset();
			}
		}
		
		$this->changeState(ParserState::DOCBLOCK);
		
		$this->docBlockDef = new DocBlockDefinition();
		
		for ($i = 0, $l = strlen($docBlock) - 1; $i <= $l; ++$i)
		{
			$this->currentToken = $docBlock[$i];
			$this->prevToken    = $i === 0  ? null : $docBlock[$i - 1];
			$this->nextToken    = $i === $l ? null : $docBlock[$i + 1];
			
			$this->stateToProcessorMap[$this->state]->process(
				$this->currentToken, $this);
		}
		
		if ($this->state !== ParserState::DOCBLOCK)
		{
			throw new ParserException(sprintf(
				'Invalid final state of the parser, ' .
				'expected [ParserState::DOCBLOCK], got [%s].',
				ParserState::getNameFromValue($this->state)
			));
		}
		
		$docBlockDef = $this->docBlockDef;
		
		$this->cleanup();
		
		return $docBlockDef;
	}
	
	/**
	 * @param  ParserState::* $newState
	 * @param  char? $token
	 * @throws ParserException
	 */
	public function changeState($newState, $token = null)
	{
		if (!isset($this->stateToProcessorMap[$newState]))
		{
			throw new ParserException(sprintf(
				'Use of unknown parser state: [%d].', $newState
			));
		}
		
		$this->state = $newState;
		
		if ($this->stateToProcessorMap[$newState] instanceof SelectionAware)
		{
			$this->stateToProcessorMap[$newState]->selected();
		}
		
		if ($token !== null)
		{
			$this->stateToProcessorMap[$newState]->process($token, $this);
		}
	}
	
	/**
	 * @param  string $description
	 * @return StateParser
	 */
	public function setShortDescription($description)
	{
		$description = trim($description);
		
		if (!empty($description))
		{
			$this->docBlockDef->shortDescription = $description;
		}
		
		return $this;
	}
	
	/**
	 * @param  string $description
	 * @return StateParser
	 */
	public function setLongDescription($description)
	{
		$description = trim($description);
		
		if (!empty($description))
		{
			$this->docBlockDef->longDescription = $description;
		}
		
		return $this;
	}
	
	/**
	 * @param  string $tagName
	 * @return StateParser
	 */
	public function addTag($tagName)
	{
		$this->docBlockDef->tagDefinitions[] = new TagDefinition($tagName);
		
		return $this;
	}
	
	/**
	 * @param  string $docString
	 * @return StateParser
	 */
	public function setTagDocString($docString)
	{
		$docString = trim($docString);
		
		if (!empty($docString))
		{
			$this->getLastTag()->docString = $docString;
		}
		
		return $this;
	}
	
	/**
	 * @param  string $annotationName
	 * @return StateParser
	 */
	public function addAnnotation($annotationName)
	{
		$this->docBlockDef->annotationDefinitions[] =
			new AnnotationDefinition($annotationName);
		
		return $this;
	}
	
	/**
	 * @param  string $annotationType
	 * @return StateParser
	 */
	public function setAnnotationType($annotationType)
	{
		$this->getLastAnnotation()->type = $annotationType;
		
		return $this;
	}
	
	/**
	 * @param  string $annotationParameter
	 * @return StateParser
	 */
	public function setAnnotationParameter($annotationParameter)
	{
		$this->getLastAnnotation()->parameter = $annotationParameter;
		
		return $this;
	}
	
	/**
	 * @param  array $arguments
	 * @return StateParser
	 */
	public function setAnnotationArguments(array $arguments)
	{
		$this->getLastAnnotation()->arguments = $arguments;
		
		return $this;
	}
	
	/**
	 * @param  array $properties
	 * @return StateParser
	 */
	public function setAnnotationProperties(array $properties)
	{
		$this->getLastAnnotation()->properties = $properties;
		
		return $this;
	}
	
	/**
	 * @param  string $docBlock
	 * @throws ParserException
	 */
	private function prepareDocBlockString(&$docBlock)
	{
		$match   = array();
		$matches = preg_match('#^/\*\*(.*?)\*/$#s', trim($docBlock), $match);
		
		if ($matches === 0)
		{
			throw new ParserException(
				'DocBlock string should begin with </**> and end with <*/>.');
		}
		
		$docBlock = trim(preg_replace("#\n\s*\* ?#", "\n",
			str_replace(array("\r\n", "\r"), "\n", $match[1])));
	}
	
	/**
	 * @return TagDefinition
	 */
	private function getLastTag()
	{
		return $this->docBlockDef->tagDefinitions[
			count($this->docBlockDef->tagDefinitions) - 1];
	}
	
	/**
	 * @return AnnotationDefinition
	 */
	private function getLastAnnotation()
	{
		return $this->docBlockDef->annotationDefinitions[
			count($this->docBlockDef->annotationDefinitions) - 1];
	}
	
	private function cleanup()
	{
		$this->docBlockDef = null;
	}
}